﻿#include "pch.h"
#include "ShaderContentControl.h"
#include "BlurEffect.h"
#include "DropShadowEffect.h"
#include "GraphicsDeviceContext.h"

using namespace std;
using namespace Dx;
using namespace XamlFx;
using namespace Platform;
using namespace Microsoft::WRL;
using namespace Windows::Foundation;
using namespace Windows::Graphics::Display;
using namespace Windows::UI::Core;
using namespace Windows::UI::Xaml;
using namespace Windows::UI::Xaml::Controls;
using namespace Windows::UI::Xaml::Media;
using namespace Windows::UI::Xaml::Markup;
using namespace Windows::ApplicationModel;
using namespace Windows::ApplicationModel::Core;
using namespace concurrency;

const wchar_t* CONTENT_CONTROL_TEMPLATE = LR"xaml_delimiter(
<ControlTemplate TargetType="ContentControl" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> 
    <Grid>
        <ContentControl UseLayoutRounding="False" 
                        Width="{Binding Content.ActualWidth,  ElementName=contentShim}" 
                        Height="{Binding Content.ActualHeight, ElementName=contentShim}"  
                        HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                        VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
            <Image x:Name="rendererImage" 
                   Stretch="Uniform" 
                   IsHitTestVisible="False"/>
        </ContentControl>
        <ContentPresenter x:Name="contentShim" 
                          Opacity="0.0" 
                          ContentTemplate="{TemplateBinding ContentTemplate}" 
                          ContentTransitions="{TemplateBinding ContentTransitions}" 
                          Content="{TemplateBinding Content}" 
                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" 
                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"/> 
    </Grid>
</ControlTemplate>
)xaml_delimiter";

inline float ConvertDipsToPixels(float dips, float dpi)
{
    static const float dipsPerInch = 96.0f;
    return (dips * dpi / dipsPerInch);
}

inline float ConvertPixelsToDips(float pixels, float dpi)
{
    static const float dipsPerInch = 96.0f;
    return pixels * dipsPerInch / dpi;
}

const float PADDING_GROW_BY_SIZE = 80.0;

template<typename T>
inline bool floating_point_equal(T a, T b) 
{
   auto diff = a - b;
   return ( diff < numeric_limits<T>::epsilon() && 
          (-diff < numeric_limits<T>::epsilon()));
}

DEPENDENCY_PROPERTY_IMP_ONCHANGE(ShaderContentControl, Effect,               XamlFx::Effect, nullptr,                        OnEffectChanged)
DEPENDENCY_PROPERTY_IMP_ONCHANGE(ShaderContentControl, EffectRenderStrategy, RenderStrategy, RenderStrategy::RenderingEvent, OnEffectRenderStrategyChanged);

void ShaderContentControl::OnEffectRenderStrategyChanged(DependencyObject^ d, DependencyPropertyChangedEventArgs^ e)
{
     auto contentControl              = safe_cast<ShaderContentControl^>(d);
     contentControl->m_renderStrategy = safe_cast<RenderStrategy>(e->NewValue);
}

void ShaderContentControl::OnEffectChanged(DependencyObject^ d, DependencyPropertyChangedEventArgs^ e)
{
    auto effect         = safe_cast<XamlFx::Effect^>(e->NewValue);
    auto contentControl = safe_cast<ShaderContentControl^>(d);
    bool isDesignMode   = DesignMode::DesignModeEnabled;

    if (effect && !isDesignMode)
    {
        effect->SetGraphicsContext(contentControl->m_gfx);
    }
}

ShaderContentControl::ShaderContentControl() : m_renderedElementDirty(true), m_ignoreNextLayoutUpdate(false), m_extraPadSize(0, 0)
{
    bool isDesignMode = DesignMode::DesignModeEnabled;

    if (!isDesignMode && !m_gfx)
    {
        m_gfx = make_shared<GraphicsDeviceContext>();
    }

    auto controlTemplate = safe_cast<ControlTemplate^>(XamlReader::Load(ref new String(CONTENT_CONTROL_TEMPLATE)));

    Template = controlTemplate;

    auto displayInformation = DisplayInformation::GetForCurrentView();

    m_loadedEventToken   = Loaded   += ref new RoutedEventHandler(this, &ShaderContentControl::OnLoaded);
    m_unloadedEventToken = Unloaded += ref new RoutedEventHandler(this, &ShaderContentControl::OnUnloaded);

    CoreApplication::Suspending            += ref new EventHandler<SuspendingEventArgs^>             (this, &ShaderContentControl::OnSuspending);
    CompositionTarget::SurfaceContentsLost += ref new EventHandler<Platform::Object^>                (this, &ShaderContentControl::OnSurfaceContentsLost);
    displayInformation->DpiChanged         += ref new TypedEventHandler<DisplayInformation^, Object^>(this, &ShaderContentControl::OnDpiChanged);
}

ShaderContentControl::~ShaderContentControl(void)
{

}

bool ShaderContentControl::ShouldReraster()
{
    auto effect = Effect;

    switch (m_renderStrategy)
    {
    case RenderStrategy::RenderingEvent:
        return true;
        break;
    case RenderStrategy::Manual:
    case RenderStrategy::Conservative:
        if (m_renderedElementDirty)
        {
            return true;
        }
        break;
    }

    return false;
}

bool ShaderContentControl::ShouldRender()
{
    auto effect = Effect;

    switch (m_renderStrategy)
    {
    case RenderStrategy::RenderingEvent:
        return true;
    case RenderStrategy::Manual:
        if (effect && !effect->IsValid())
        {
            return true;
        }
        return false;
    case RenderStrategy::Conservative:
        if (effect && (!effect->IsValid() || m_renderedElementDirty))
        {
            return true;
        }
        break;
    }

    return false;
}

void ShaderContentControl::OnLoaded(Object^ sender, RoutedEventArgs^ e)
{
    m_renderCallbackToken = CompositionTarget::Rendering += ref new EventHandler<Object^>(this, &ShaderContentControl::CompositionTargetRender);
}

void ShaderContentControl::OnUnloaded(Object^ sender, RoutedEventArgs^ e)
{
    CompositionTarget::Rendering -= m_renderCallbackToken;
}

void ShaderContentControl::InitializeSurfaceRenderer(Dx::SizeU size)
{
    if (m_xamlSurfaceRenderer)
    {
        auto currentSize = m_xamlSurfaceRenderer->GetSize();

        if (currentSize.Width  == size.Width && 
            currentSize.Height == size.Height)
        {
            return;
        }
    } 

#if _DEBUG
    auto debug = ref new Platform::String();
    debug = "Creating new surface: " + size.Width.ToString() + "x" + size.Height.ToString() + "\n";
    OutputDebugString(debug->Data());
#endif

    m_xamlSurfaceRenderer = ref new XamlSurfaceRenderer(m_gfx, size, false);
}


void ShaderContentControl::InvalidateRaster()
{
    m_renderedElementDirty = true;
}

void ShaderContentControl::Render()
{
    bool isDesignMode = DesignMode::DesignModeEnabled;

    auto effect = Effect;

    if (isDesignMode && m_contentShim)
    {
        m_contentShim->Opacity = 1.0;
    }

    if (m_isRendering || !m_contentShim || !effect || isDesignMode || !m_renderedElement)
    {
        return;
    }
   
    m_isRendering = true;
   
    effect->SetUIElement(m_renderedElement);
 
    auto diplayInformation  = DisplayInformation::GetForCurrentView();
    float logicalDpi        = diplayInformation->LogicalDpi;
    Thickness effectPadding = effect->Padding;

    task<Dx::SizeU> startTask;

    if (ShouldReraster() || !m_xamlSurfaceRenderer)
    {
        startTask = effect->RenderAsync().then([this, logicalDpi, effectPadding](Dx::SizeU size)
        {
            m_renderedElementDirty = false;
            return size;
        });
    }
    else
    {
        startTask = create_task([this, effect]
        {
            return effect->GetElementPixelSize();
        }, task_continuation_context::use_current());
    }

    startTask.then([this, logicalDpi, effectPadding](Dx::SizeU size)
    {
        Dx::SizeU adjustedSize((size.Width  + static_cast<unsigned>(ConvertDipsToPixels(static_cast<float>(effectPadding.Left + effectPadding.Right),  logicalDpi))), 
                               (size.Height + static_cast<unsigned>(ConvertDipsToPixels(static_cast<float>(effectPadding.Top  + effectPadding.Bottom), logicalDpi))));

        Dx::SizeU paddedSize = adjustedSize;

        paddedSize.Width  = static_cast<unsigned>(ceil(adjustedSize.Width  / PADDING_GROW_BY_SIZE) * PADDING_GROW_BY_SIZE);
        paddedSize.Height = static_cast<unsigned>(ceil(adjustedSize.Height / PADDING_GROW_BY_SIZE) * PADDING_GROW_BY_SIZE);


        m_extraPadSize.Width  = ConvertPixelsToDips(static_cast<float>(paddedSize.Width  - adjustedSize.Width ), logicalDpi);
        m_extraPadSize.Height = ConvertPixelsToDips(static_cast<float>(paddedSize.Height - adjustedSize.Height), logicalDpi);

        InitializeSurfaceRenderer(paddedSize);
    }, task_continuation_context::use_current()).then([this, effect, logicalDpi, effectPadding]
    {
        Dxgi::Surface dxgiSurface;
        Dx::Point2F   offset;

        m_xamlSurfaceRenderer->BeginDraw(dxgiSurface, offset);
     
        offset.X = ConvertPixelsToDips(offset.X, logicalDpi) + static_cast<float>(effectPadding.Left) + (m_extraPadSize.Width  / 2.0f);
        offset.Y = ConvertPixelsToDips(offset.Y, logicalDpi) + static_cast<float>(effectPadding.Top)  + (m_extraPadSize.Height / 2.0f);
       
        return effect->ApplyEffectAsync(dxgiSurface, offset);
    }, task_continuation_context::use_current()).then([this, effectPadding]
    {
        m_xamlSurfaceRenderer->EndDraw();

        if (m_image->Source != m_xamlSurfaceRenderer)
        {
            m_image->Source = m_xamlSurfaceRenderer;
        }

        SizeRenderedImage(effectPadding);
    }, task_continuation_context::use_current()).then([this](task<void> t)
    {
        try
        {
            t.get();
        }
        catch (Platform::Exception^)
        {
        }
        catch (std::exception&)
        {
        }

        m_isRendering = false;
    }, task_continuation_context::use_current());
}

void ShaderContentControl::CompositionTargetRender(Object^ sender, Object^ e)
{
    if (ShouldRender())
    {
        Render();
    }
}

void ShaderContentControl::OnContentChanged(Object^ oldContent, Object^ newContent)
{
    if (newContent)
    {
        m_renderedElement = safe_cast<UIElement^>(newContent);
    }
    else
    {
        m_renderedElement = nullptr;
    }
}

void ShaderContentControl::OnApplyTemplate()
{
    m_image       = safe_cast<Image^>            (GetTemplateChild("rendererImage"));
    m_contentShim = safe_cast<ContentPresenter^> (GetTemplateChild("contentShim"));

    bool isDesignMode = DesignMode::DesignModeEnabled;

    if (m_contentShim && m_contentShimLayoutUpdated.Value)
    {
        m_contentShim->LayoutUpdated -= m_contentShimLayoutUpdated;
        m_contentShimLayoutUpdated.Value = 0;
    }

    if (isDesignMode && m_contentShim)
    {
        m_contentShim->Opacity = 1.0;
    }

    m_contentShimLayoutUpdated = m_contentShim->LayoutUpdated += ref new EventHandler<Object^>(this, &ShaderContentControl::OnLayoutUpdated);
}


void ShaderContentControl::SizeRenderedImage(Thickness padding)
{
    if (!m_image || !Effect)
    {
        return;
    }

    auto diplayInformation  = DisplayInformation::GetForCurrentView();

    auto thickness = Thickness(((static_cast<float>(-padding.Left  ) - m_extraPadSize.Width  / 2.0f)),
                               ((static_cast<float>(-padding.Top   ) - m_extraPadSize.Height / 2.0f)),
                               ((static_cast<float>(-padding.Right ) - m_extraPadSize.Width  / 2.0f)),
                               ((static_cast<float>(-padding.Bottom) - m_extraPadSize.Height / 2.0f)));
   
    auto currentThickness = m_image->Margin;
     
    if (!floating_point_equal(currentThickness.Left,   thickness.Left  ) ||
        !floating_point_equal(currentThickness.Right,  thickness.Right ) ||
        !floating_point_equal(currentThickness.Top,    thickness.Top   ) ||
        !floating_point_equal(currentThickness.Bottom, thickness.Bottom))
    {
        m_image->Margin = thickness;
        m_ignoreNextLayoutUpdate = true;
    }
}

void ShaderContentControl::OnLayoutUpdated(Object^ sender, Object^ e)
{
    bool ignoreNext = m_ignoreNextLayoutUpdate;
    m_ignoreNextLayoutUpdate = false;

    if (ignoreNext)
    {
        return;
    }

    if (m_renderStrategy != RenderStrategy::Manual)
    {
        m_renderedElementDirty = true;
    }
}

void ShaderContentControl::OnDpiChanged(DisplayInformation^ sender, Object^ args)
{
    m_gfx->SetDPI(sender->LogicalDpi, sender->LogicalDpi);
}

void ShaderContentControl::OnSurfaceContentsLost(Object^ sender, Object ^args)
{
    m_xamlSurfaceRenderer = nullptr;
    auto effect = Effect;

    if (effect)
    {
        effect->ResetResources();
        m_isRendering = false;
    }
}

void ShaderContentControl::OnSuspending(Object^ sender, SuspendingEventArgs^ args)
{
    SuspendingDeferral^ deferral = args->SuspendingOperation->GetDeferral();
    
    if (m_gfx)
    {
        ComPtr<ID3D11Device> dxgiDevice = m_gfx->GetD3DDevice().Get();
        ComPtr<IDXGIDevice3> dxgiDevice3;

        dxgiDevice.As(&dxgiDevice3);

        dxgiDevice3->Trim();
    }

    deferral->Complete();
}
